在前面的章节中,我们学习了诸多关于 Vite 使用和项目搭建的内容,接下来让我们将目光集中到 Vite 本身的架构上,分析它是如何站在巨人的肩膀上实现出来的。所谓的巨人,指的就是 Vite 底层所深度使用的两个构建引擎——Esbuild和Rollup。
那么,这两个构建引擎对于 Vite 来说究竟有多重要?在 Vite 的架构中,两者各自扮演了什么样的角色?在本小节的内容,我和你一起拆解 Vite 的双引擎架构,深入分析Esbuild和Rollup究竟在 Vite 中做了些什么。
# Vite 架构图
很多人对 Vite 的双引擎架构仅仅停留在开发阶段使用 Esbuild,生产环境用 Rollup的阶段,殊不知,Vite 真正的架构却远远没有这么简单。一图胜千言,这里放一张 Vite 架构图:

相信对于 Vite 的双引擎架构,你可以从图中略窥一二。在接下来的内容中,我会围绕这张架构图展开双引擎的介绍,到时候对于这份架构图你会理解的更加透彻。
# 性能利器——Esbuild
首先,不可否认的是,Esbuild的确是 Vite 高性能的得力助手,在很多关键的构建阶段让 Vite 能够获得相当优异的性能,如果这些阶段用传统的打包器/编译器来完成的话,开发体验要下降一大截。接下来我们梳理一下在 Vite 的构建体系中,Esbuild 到底发挥了哪些作用。
# 一、依赖预构建——作为 Bundle 工具
首先是开发阶段的依赖预构建阶段。

对于动辄几百 MB 甚至上 GB 的 node_modules 依赖,一般会远远超过项目源代码的大小,相信大家都深有体会。如果这些依赖直接在 Vite 中使用,会出现一系列的问题,这些问题我们在依赖预构建的小节已经详细分析过,主要是 ESM 格式的兼容性问题和海量请求的问题,不再赘述。总而言之,对于第三方依赖,需要在应用启动前进行打包并且转换为 ESM 格式。
Vite 1.x 版本中使用 Rollup 来做这件事情,但 Esbuild 的性能实在是太恐怖了,Vite 2.x 果断采用 Esbuild 来完成第三方依赖的预构建,至于性能到底有多强,大家可以参照它与传统打包工具的性能对比图:

当然,Esbuild 作为打包工具也有一些缺点,比如:
- 不支持降级到
ES5的代码。这意味着在低端浏览器代码会跑不起来。 - 不支持
const enum等语法。这意味着单独使用这些语法在 esbuild 中会直接抛错。 - 不提供操作打包产物的接口,像 Rollup 中灵活处理打包产物的能力(如
renderChunk钩子)在 Esbuild 当中完全没有。 - 不支持自定义 Code Splitting 策略。传统的 Webpack 和 Rollup 都提供了自定义拆包策略的 API,而 Esbuild 并未提供,从而降级了拆包优化的灵活性。
尽管 Esbuild 作为一个社区新兴的明星项目,有如此多的局限性,但依然不妨碍 Vite 在开发阶段使用它成功启动项目并获得极致的性能提升,生产环境处于稳定性考虑当然是采用功能更加丰富、生态更加成熟的 Rollup 来作为依赖打包工具了。
# 二、单文件编译——作为 TS 和 JSX 编译工具
在依赖预构建的阶段 Esbuild 作为 Bundler 的角色存在,而在 TS(X)/JS(X) 单文件编译上面,Vite 也使用了 Esbuild 来进行语法转译,也就是将 Esbuild 作为 Transformer 来使用。大家可以在架构图中Vite Plugin Pipeline部分注意到:

也就是说,Esbuild 转译 TS 或者 JSX 的能力通过 Vite 插件提供,这个 Vite 插件在开发环境和生产环境都会执行,因此,我们可以得出下面这个结论:
Vite 已经将 Esbuild 的 Transformer 能力用到了生产环境。
这部分能力是用来替换原先 Babel 或者 TSC 的功能,因为无论是 Babel 还是 TSC,在性能方面都是有问题的,大家对这两个工具普遍的认知都是: 慢,太慢了。
当 Vite 使用 Esbuild 做单文件编译之后,提升可以说相当大了,我们以一个巨大的为 50 多 MB 的纯代码文件为例,来对比一下Esbuild、Babel、TSC 包括 SWC 的编译性能:

在 Esbuild Transfomer 带来巨大性能提升的同时,它也随之带来了一些局限性,最大的局限性就在于 TS 中的类型检查问题。由于 Esbuild 并没有实现 TS 的类型系统,在编译 TS(或者 TSX) 文件时仅仅抹掉了类型相关的代码,暂时没有能力实现类型检查。这也就是为什么在快速上手小节的最后,我让大家注意到初始化工程的构建脚本,vite build之前会先执行tsc命令,也就是借助 TS 官方的编译器进行类型检查。
当然,对于类型问题,我更推荐大家使用 TS 的编辑器插件,让问题能在开发阶段就能早早地暴露出来并被解决,而不是项目要打包上线的时候才被感知到。
# 三、代码压缩——作为压缩工具
Vite 从 2.6 版本开始,就官宣默认使用 Esbuild 来进行生产环境的代码压缩,包括 JS 代码和 CSS 代码。
从架构图中可以看到,在生产环境中 Esbuild 压缩器通过插件的形式融入到了 Rollup 的打包流程中:

那为什么 Vite 要将 Esbuild 作为生产环境下默认的压缩工具呢?因为压缩效率实在太高了!
传统的方式都是使用 Terser 这种 JS 开发的压缩器来实现,在 Webpack 或者 Rollup 中作为一个 Plugin 来完成代码打包后的压缩混淆的工作。但 Terser 其实是很慢的,主要有两个原因:
- 压缩这项工作涉及到大量 AST 操作,并且在传统的构建流程中,AST 在各个工具之间无法共享,比如 Terser 就无法与 Babel 共享同一个 AST,造成了很多重复解析的过程。
- JS 本身属于解释性 + JIT(即时编译) 的语言,对于压缩这种 CPU 密集型的工作,其性能远远比不上 Golang 这种原生语言。
正是这些原因才使得 Esbuil
